package main

import (
	"fmt"
	"time"
	"strconv"
	"sync"
)

var(
	//犯规的人（由犯规考生写入，由巡考读出）
	foulers = make(chan string, 100)

	/*
	车道（信号量Semaphore）:控制考试信号量
	想要获取考试资格必须先向该管道写入一个数据，考试结束再从该管道读出一个数据
	以这种方式将考试并发数控制在5）
	*/
	chLanes = make(chan int, 5)

	//考试成绩单（姓名为键，分数为值）
	scoreMap       = make(map[string]int)

	//写入成绩单的互斥锁（map不允许并发写入，否则panic）
	scoreMapMutex sync.RWMutex
)

/*
考试结果
数据表的一行对应一个结构体实例
每列对应结构体实例的一个属性
*/
type ExamResult struct {
	//对应数据表的id列
	Id int `db:"id"`

	//对应数据表的name列
	Name string `db:"name"`

	//对应数据表的score列
	Score int `db:"score"`
}

/*
查询成绩
内部逻辑使用二级缓存：优先读取Redis缓存，其次读取	MySQL核心数据库
*/
func QueryScore(name string) {
	//score, err := ReadRedis(name)

	/*
	查询Redis缓存是否有数据：get 张三，希望将结果转换为int
	得到【结果/错误对】，有错误就是没结果（或无效结果），没错误就是有结果
	*/
	score, err := DoRedisCommand("get "+name, "int")
	if err != nil {
		fmt.Println("未能从Redis中获取数据", err)
		//select * from score where name = "张三"

		/*
		按照API要求，构造【name:"张三"】这个键值对，作为查询条件
		为什么map值的类型是interface{}呢？因为你还有可能：
		select * from score where name = "张三" and id = 123
		此时就需要两个键值对：【name:"张三"】【id:123】
		只有空接口才能兼容所有类型
		*/
		argsMap := make(map[string]interface{}, 0)
		argsMap["name"] = name

		/*
		查询结果中的每一行对应一个ExamResult对象
		查询结果肯定是若干行（考虑到可能查询到多条记录），所以用切片接收
		预定义接收的数据切片
		*/
		rets := make([]ExamResult, 0)
		/*
		score=表名，argsMap=要查询的条件map，&rets查询结果的接收地址
		如果有err，说明查询失败，没有错误，则结果在给定的地址中
		*/
		err = ReadMysql("score", argsMap, &rets)
		HandleError(err,`ReadMysql("score", argsMap, &dest)`)
		//打印结果
		fmt.Println(rets)

		/*缓存数据到Redis*/
		//WriteRedis(name, results[0].Score)
		//拿到查询结果中的第一条（正常情况就只有一条，此处不考虑重名的情况）
		examResult := rets[0]

		//拿到姓名和分数
		name := examResult.Name
		score := strconv.Itoa(examResult.Score)

		//构造Redis命令：set 张三 80
		cmd := "set " + name + " " + score
		//fmt.Println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",name,score,cmd)

		//执行Redis命令：set 张三 80，结果是ok其类型是string
		DoRedisCommand(cmd,"string")

	} else {
		//没有错误，说明已经从Redis中查到数据
		fmt.Println(name, ":", score)
	}

	//一个小破工具方法，不要管调度的事情，做好自己的成绩查询工作即可
	//wg.Done()
}

/*
巡考逻辑
巡考协程没有在等待组中注册，其生命周期与主协程相同
*/
func Patrol() {
	/*
	设置周期性运行的秒表
	时间设置为考试时间的1/5，每轮考试时间内可以巡视5次，这样如果5人都犯规，能确保全部读出
	*/
	ticker := time.NewTicker(200 * time.Millisecond)

	/*巡视组不停地巡视考场*/
	for {
		//fmt.Println("巡视组出动了!")

		/*
		有人犯规或无人犯规，选择一条能走通的路
		所有case都走不通才会去看default，多个case能走通，就随机选一条
		*/
		select {
		//如果犯规者管道能读出数据，说明有人犯规
		case f := <-foulers:
			fmt.Println(f, "犯规滚蛋!!!")
			//想办法结束该考生的协程

		//如果犯规者管道【不能】读出数据，说明考场纪律良好
		default:
			fmt.Println("考场纪律良好！")
		}

		//阻塞200毫秒
		<-ticker.C
	}
}

/*
考试逻辑
*/
func TakeExam(name string) {

	/*阻塞等待至车道chLanes出现空位，才能写入数据，才能获得考试资格*/
	chLanes <- 123
	fmt.Println()
	fmt.Println(name, "开始考试")

	//正式参加考试的考生，记录一份名单，留作日后查询成绩使用
	names = append(names, name)

	/*随机生成考试成绩，低于10分算违规*/
	score := GetRandomInt(0, 100)
	if score < 10 {
		//将违规者姓名丢入违规管道，供巡考通报批评
		foulers <- name
		fmt.Println(name, "犯规了！")
	}

	/*
	同步地将成绩写入map
	之所要要抢锁才能写入，是因为map不允许并发（concurrent）写入
	*/
	scoreMapMutex.Lock()
	scoreMap[name] = score
	scoreMapMutex.Unlock()

	//模拟考试耗时
	<-time.After(1 * time.Second)

	/*从车道管道读出一个数据，腾出一个位置，给后来的协程*/
	fmt.Println(name, "考试结束")
	<-chLanes

	//工具不方法不该做调度
	//从等待组中注销
	//wg.Done()
}

/*
录入成绩
遍历scoreMap中的名字和分数，分别插入到score表的name和score字段
*/
func InputScore2DB() {
	for name, score := range scoreMap {

		//将来要执行的SQL语句
		//insert into score(name,score) valuse("你妹",59);

		//构造字段和值的对应关系字典
		dataMap := make(map[string]interface{})
		dataMap["name"] = name
		dataMap["score"] = score

		//将数据插入记录
		WriteMysql("score", dataMap)
	}
}